package utils;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.collections4.MapUtils;
import org.bson.Document;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;

import jodd.net.URLCoder;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GiteeUtils {
	
	private static ObjectMapper objMapper = new ObjectMapper();
	public static final int PAGE_SIZE = 100;
	private static final int TIMEOUT = 20;
	
	private GiteeUtils() {}
	
	public static void download(String repoOwner, String repoName, String repoBranch
			, String mongoDatabase, String commitsCollection
			, LocalDate startDate, LocalDate endDate) {
	
		MongoDatabase db = MongoUtils.getDatabase(mongoDatabase);
		MongoCollection<Document> commitsColl = db.getCollection(commitsCollection);
		commitsColl.createIndex(new Document("commit.committer.date",1));
		commitsColl.createIndex(new Document("sha",1));
		
		long days = startDate.until(endDate, ChronoUnit.DAYS);
		ExecutorService threadPool = Executors.newFixedThreadPool(2);
		AtomicInteger counter = new AtomicInteger(0);
		AtomicInteger commitsCount = new AtomicInteger(0);
		int step = 30;
		for(int i=0;i<=days;i+=step) {
			String startTime = startDate.plusDays(i).toString()+" 00:00:00";
			String endTime = startDate.plusDays(Math.min(days, i+step-1)).toString()+" 23:59:59";
			threadPool.execute(()->{
				 
				log.info("getting commits from {} to {}:", startTime, endTime);
				// 记录commits数量
				AtomicBoolean isDone = new AtomicBoolean(false);
				// 获取提交记录
				GiteeUtils.getRepoData(repoOwner,repoName,new String[] {repoBranch}, startTime, endTime, commits->{
					if(!commits.isEmpty()) {
						commitsCount.getAndAdd(commits.size());
						commitsColl.deleteMany(new Document("sha", new Document("$in", commits.stream().map(commit->commit.get("sha")).toList())));
						commitsColl.insertMany(commits.stream().map(commit->new Document(commit)
								.append("repoOwner", repoOwner)
								.append("repo", repoName)
								.append("branch", repoBranch)).toList());
					}
					if( commits.size() < GiteeUtils.PAGE_SIZE) {
						isDone.getAndSet(true);
					}
				});
				if(isDone.get()) {
					log.info("get commits done:{} to {}", startTime, endTime);
				}
				counter.getAndAdd(step);
			});
		}
		threadPool.shutdown();
		while(!threadPool.isTerminated()) {
			log.info("finished:{}/{} days, {} commits", counter.get(), days, commitsCount.get());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		
		
	}
	public static void updateCommitDetails(String repoOwner, String repoName, String repoBranch
			, String mongoDatabase, String commitsCollection
			, LocalDate startDate, LocalDate endDate) {
		MongoDatabase db = MongoUtils.getDatabase(mongoDatabase);
		MongoCollection<Document> commitsColl = db.getCollection(commitsCollection);
		var condition =Document.parse(String.format("""
				{
				$and: [
				{"repoOwner": "%s"},
				{"repo": "%s"},
				{"branch": "%s"},
				{"stat": {"$exists": false}},
				{$expr: {"$gte": [{$toDate:"$commit.committer.date"}, {$toDate:"%sT00:00:00.000Z"}]}},
				{$expr: {"$lte": [{$toDate:"$commit.committer.date"}, {$toDate:"%sT23:59:59.999Z"}]}}
				]
				}
				""", repoOwner, repoName, repoBranch, startDate.toString(), endDate.toString()));
		long totalCommits = commitsColl.countDocuments(condition);
		log.info("{} commits to update", totalCommits);
		AtomicInteger no= new AtomicInteger(0);
		int limit =100;
		Stream.iterate(0, i->i<totalCommits, i->i+limit).parallel().forEach(i->{
			
			try(var commitsCursor = commitsColl.find(condition).sort(new Document("commit.committer.date",1)).skip(i).limit(limit).iterator()){
				while(commitsCursor.hasNext()) {
					var doc = commitsCursor.next();
					var commitInfo = GiteeUtils.getCommitInfo(repoOwner, repoName, doc.getString("sha"));
					commitsColl.updateOne(new Document("sha", doc.getString("sha")), new Document("$set", new Document("stat", commitInfo.get("stat"))));
					no.getAndIncrement();
					if(no.get()%1000==0) {
						log.info("update {} commits of {}", no.get(), totalCommits);
					}
				}
			}
		});
	}
	public static void getRepoData(String owner, String repo, String start, String end
			, Consumer<List<Map<String, Object>>> commitsConsumer ) {
		List<String> branches = getAllBranches(owner,repo );
		getRepoData(owner,repo, branches.toArray(new String[0]),start, end,   commitsConsumer );
		
	}
	public static void getRepoData(String owner, String repo, String[] branches,  String start, String end
			, Consumer<List<Map<String, Object>>> commitsConsumer ) {
		log.debug("getting repo:"+repo);
		log.debug("branches:"+ String.join(",", branches));
		for(String branch: branches) {
			// 获取提交记录
			getCommits(owner,repo, branch, start, end, commitsConsumer );
			
		}
		
	}
	public static String  getFileContent(String owner, String repo, String fileSha ) {
		String url = String.format("https://gitee.com/api/v5/repos/%s/%s/git/blobs/%s?access_token=%s"
				, owner
				, repo
				, fileSha
				, getToken());
		String content = null;
		try {
			Map<String, Object> json = getJSON(url);
			content = MapUtils.getString(json, "content");
			String encoding =  MapUtils.getString(json, "encoding");
			if(encoding.equalsIgnoreCase("base64")) {
				content = new String(Base64.getDecoder().decode(content), StandardCharsets.UTF_8);
			}
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		}
		return content;
	}
	public static Map<String, String> getFiles(String owner, String repo, String branch ) {
		String url = String.format("https://gitee.com/api/v5/repos/%s/%s/git/trees/%s?recursive=1&access_token=%s"
				, owner
				, repo
				, branch
				, getToken());
		try {
			Map<String, Object> json  = getJSON(url);
			Object filesTree = json.get("tree");
			return ((List<Map<String, String>>)filesTree).stream()
					.collect(Collectors.toMap(f->f.get("path")
							,f->f.get("sha")));
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		}
		return Collections.emptyMap();
	}
	public static Map<String, Object> getCommitInfo(String owner, String repo, String commit ) {
		String url = String.format("https://gitee.com/api/v5/repos/%s/%s/commits/%s?access_token=%s"
				, owner
				, repo
				, commit
				, getToken());
		try {
			return getJSON(url);
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		}
		return  Collections.emptyMap();
	}

	public static void getCommits(String owner, String repo, String branch, String since, String until
			, Consumer<List<Map<String, Object>>> commitsConsumer ) {
		String urlFormat =  "https://gitee.com/api/v5/repos/%s/%s/commits?access_token=%s&sha=%s&since=%s&until=%s&page=%s&per_page=%s";
		int page = 1;
		while(true) {
			try {
				String requestUrl =  String.format(urlFormat, owner, repo, getToken(), branch
						, URLCoder.encode(since, StandardCharsets.UTF_8)
						, URLCoder.encode(until, StandardCharsets.UTF_8)
						, page
						, PAGE_SIZE);
				List<Map<String, Object>> commits = getJSONArray(requestUrl);
				commitsConsumer.accept(commits);
				if(commits.size() == PAGE_SIZE) {
					page++;
				}else {
					break;
				}
			} catch (IOException|InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static List<String> getAllBranches(String owner, String repo){
		String url = String.format("https://gitee.com/api/v5/repos/%s/%s/branches?access_token=%s"
				, owner
				, repo
				, getToken());
		try {
			List<Map<String, Object>> json = getJSONArray(url);
			return json.stream()
					.map(m -> m.get("name").toString())
					.toList();
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		}
		return Collections.emptyList();
		
	}
	
	private static Map<String, Object> getJSON(String url) throws IOException, InterruptedException     {
		log.debug("getting JSON from url:"+url);
		HttpRequest request = HttpRequest.newBuilder(URI.create(url))
				.timeout(Duration.ofSeconds(TIMEOUT))
				.GET()
				.build();
		HttpResponse<String> resp = HttpClient.newHttpClient()
				.send(request , HttpResponse.BodyHandlers.ofString());
		if(resp.statusCode() == 200) {
			return objMapper.readValue(resp.body(), new TypeReference<Map<String, Object>>() {});
		}else {
			throw new RuntimeException(String.format("http request failed,url:%s,status code:%d",url,resp.statusCode()));
		}
	}
	private static List<Map<String, Object>> getJSONArray(String url) throws IOException, InterruptedException     {
		log.debug("getting JSON array from url:"+url);
		HttpRequest request = HttpRequest.newBuilder(URI.create(url))
				.timeout(Duration.ofSeconds(TIMEOUT))
				.GET()
				.build();
		HttpResponse<String> resp = HttpClient.newHttpClient()
				.send(request , HttpResponse.BodyHandlers.ofString());
		if(resp.statusCode() == 200) {
			
			return objMapper.readValue(resp.body(), new TypeReference<List<Map<String, Object>>>() {});
		}else {
			throw new RuntimeException(String.format("http request failed,url:%s,status code:%d",url,resp.statusCode()));
		}
	}
	
	public static String getToken() {
		return PropertyUtils.get("gitee.token");
	}
}
